package org.irri.smta.util;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import edu.vt.middleware.password.CharacterCharacteristicsRule;
import edu.vt.middleware.password.DigitCharacterRule;
import edu.vt.middleware.password.LengthRule;
import edu.vt.middleware.password.LowercaseCharacterRule;
import edu.vt.middleware.password.NonAlphanumericCharacterRule;
import edu.vt.middleware.password.Password;
import edu.vt.middleware.password.PasswordData;
import edu.vt.middleware.password.PasswordValidator;
import edu.vt.middleware.password.Rule;
import edu.vt.middleware.password.RuleResult;
import edu.vt.middleware.password.UppercaseCharacterRule;
import edu.vt.middleware.password.WhitespaceRule;

public class PasswordUtils implements Serializable {

	private static final long serialVersionUID = 1L;
	private static final String ALPHA_CHARACTERS = "abcdefghijklmnopqrstuvwxyz";
	private static final String NUMBERS = "0123456789";
	private static final String SYMBOLS = "$%&*@#!_:~";
	
	/**
	 * Calculates the strength of the password based on certain factors such as.
	 * <ul>
	 * <li>Length</li
	 * <li>Number of upper case letters</li>
	 * <li>Number of lower case letters</li>
	 * <li>Number of digits</li>
	 * <li>Number of digits or symbols in the middle</li>
	 * <li>Any repeating characters</li>
	 * <li>Sequences</li>
	 * 
	 * It also adds bonuses if the password is 8 characters or more and it has a
	 * certain combination of lowercase, uppercase, digits and symbols.
	 * 
	 * @param password
	 *            the password to check the strength of
	 * @return the strength which is a value between zero and one hundred (0 -
	 *         100).
	 */
	public static Integer getStrength(final String password) {
		if (password == null) {
			return 0;
		}
		
		Integer strength = 0;
		
		// Length
		strength += (password.length() * 4);

		// Upper case letters
		final Integer upperCase = matches(password, "[A-Z]");

		if (upperCase > 0) {
			strength += ((password.length() - upperCase) * 2);
		}

		// Lower case letters
		final Integer lowerCase = matches(password, "[a-z]");

		if (lowerCase > 0) {
			strength += ((password.length() - lowerCase) * 2);
		}

		// Numbers
		final Integer numbers = matches(password, "[0-9]");

		if (numbers > 0) {
			strength += (numbers * 4);
		}

		if (numbers == password.length()) {
			strength -= numbers;
		}

		// Special characters
		final Integer specialCharacters = matches(password,
				"[:,!,@,#,$,%,^,&,*,?,_,~]");

		if (specialCharacters > 0) {
			strength += (specialCharacters * 6);
		}

		// Letters only
		final Integer letters = matches(password, "[a-z|A-Z]");

		if (letters == password.length()) {
			strength -= letters;
		}

		// Middle numbers
		Integer middles = 0;

		for (Integer j = 0; j < NUMBERS.length(); ++j) {
			if ((password.indexOf(NUMBERS.charAt(j)) > 0)
					&& (password.indexOf(NUMBERS.charAt(j)) < password.length() - 1)) {
				++middles;
			}
		}

		// Middle symbols
		for (Integer j = 0; j < SYMBOLS.length(); ++j) {
			if ((password.indexOf(SYMBOLS.charAt(j)) > 0)
					&& (password.indexOf(SYMBOLS.charAt(j)) < password.length())) {
				++middles;
			}
		}

		strength += (middles * 2);

		// Repeat characters
		final List<Character> done = new ArrayList<Character>();
		Integer repeats = 0;

		for (Integer i = 0; i < password.length(); ++i) {
			Integer matches = 0;

			for (Integer j = 0; j < password.length(); ++j) {
				if (password.toLowerCase().charAt(i) == password.toLowerCase()
						.charAt(j)
						&& !done.contains(password.toLowerCase().charAt(i))) {
					++matches;
				}
			}

			if (matches > 1) {
				strength -= (matches * (matches - 1));
				repeats += (matches * (matches - 1));
				done.add(password.toLowerCase().charAt(i));
			}
		}

		// Consecutive upper case letters
		final Integer consequtiveUpperCaseLetters = matches(password, "[A-Z]{2}");
		strength -= (consequtiveUpperCaseLetters * 2);

		// Consecutive lower case letters
		final Integer consequtiveLowerCaseLetters = matches(password, "[a-z]{2}");
		strength -= (consequtiveLowerCaseLetters * 2);

		// Consecutive numbers
		final Integer consequtiveNumbers = matches(password, "[0-9]{2}");
		strength -= (consequtiveNumbers * 2);

		// Sequential characters
		for (Integer i = 0; i < password.length(); ++i) {
			final Integer index = ALPHA_CHARACTERS.indexOf(String.valueOf(
					password.charAt(i)).toLowerCase());

			if (index != -1 && index < ALPHA_CHARACTERS.length() - 3) {
				final String forwardSequence = ALPHA_CHARACTERS.substring(
						index, index + 3);
				final String reverseSequence = new StringBuffer(forwardSequence)
						.reverse().toString();

				if (password.indexOf(forwardSequence) != -1
						|| password.indexOf(reverseSequence) != -1) {
					strength -= 3;
				}
			}
		}

		// Sequential numbers
		for (Integer i = 0; i < password.length(); ++i) {
			final Integer index = NUMBERS.indexOf(String
					.valueOf(password.charAt(i)).toLowerCase());

			if (index != -1 && index < NUMBERS.length() - 3) {
				final String forwardSequence = NUMBERS.substring(index,
						index + 3);
				final String reverseSequence = new StringBuffer(forwardSequence)
						.reverse().toString();

				if (password.indexOf(forwardSequence) != -1
						|| password.indexOf(reverseSequence) != -1) {
					strength -= 3;
				}
			}
		}

		// Bonuses
		Integer bonus = 0;

		if (password.length() >= 8) {
			++bonus;

			if ((upperCase.doubleValue() / password.length()) >= 0.25) {
				++bonus;
			}

			if ((lowerCase.doubleValue() / password.length()) >= 0.25) {
				++bonus;
			}

			if ((numbers.doubleValue() / password.length()) >= 0.25) {
				++bonus;
			}

			if ((specialCharacters.doubleValue() / password.length()) >= 0.25) {
				++bonus;
			}
		}

		strength += (bonus * 2);

		if (strength < 0) {
			strength = 0;
		} else if (strength > 100) {
			strength = 100;
		}

		return strength;
	}

	private static Integer matches(final String string, final String regexPattern) {
		Integer matches = 0;
		final Pattern pattern = Pattern.compile(regexPattern);
		final Matcher matcher = pattern.matcher(string);

		while (matcher.find()) {
			++matches;
		}

		return matches;
	}

	public static Boolean isValid(String password) {
		// password must be between 6 and 30 chars long
		LengthRule lengthRule = new LengthRule(6, 30);

		// don't allow whitespace
		WhitespaceRule whitespaceRule = new WhitespaceRule();

		// control allowed characters
		CharacterCharacteristicsRule charRule = new CharacterCharacteristicsRule();
		// require at least 1 digit
		charRule.getRules().add(new DigitCharacterRule(1));
		// require at least 1 non-alphanumeric char
		charRule.getRules().add(new NonAlphanumericCharacterRule(1));
		// require at least 1 upper case char
		charRule.getRules().add(new UppercaseCharacterRule(1));
		// require at least 1 lower case char
		charRule.getRules().add(new LowercaseCharacterRule(1));
		// require at least 3 of the previous rules be met
		charRule.setNumberOfCharacteristics(3);

		// group all rules together in a List
		List<Rule> ruleList = new ArrayList<Rule>();
		ruleList.add(lengthRule);
		ruleList.add(whitespaceRule);
		ruleList.add(charRule);
		
		PasswordValidator validator = new PasswordValidator(ruleList);
		PasswordData passwordData = new PasswordData(new Password(password));
		RuleResult ruleResult = validator.validate(passwordData);
		
		return ruleResult.isValid();
	}
}
